#!/bin/bash
# This script rebuilds SRPMs into RPMs using a local repository
# Author: Mikhail Novosyolov <m.novosyolov@rosalinux.ru>, 2021-2023
# License: GPLv3

# fail on errors
set -e
# explicitly enable file globbing
set +f
# fail on not defined variables
set -u
# export variables and functions to easify parallelization
set -a

# Позволять другим пользователям читать результаты сборки
umask 0022

MOCK_USER="${MOCK_USER:-offlinerebuilder}"
RESULTS_DIR="${RESULTS_DIR:-/var/lib/${MOCK_USER}/_results}"
MOCK_CONFIG="${MOCK_CONFIG:-$PWD/default.cfg}"
MOCK_CONFIG_DIRECTORY=""
SRPMS_DIR="${SRPMS_DIR:-/mnt/repo-srpms}"
LOGS_DIR="${LOGS_DIR:-$RESULTS_DIR/logs}"
RESULTING_RPMS_TMP="${RESULTING_RPMS_TMP:-$RESULTS_DIR/rpms-tmp}"
RESULTING_RPMS_DIR="${RESULTING_RPMS_DIR:-$RESULTS_DIR/rpms-release}"
RESULTING_DEBUG_DIR="${RESULTING_DEBUG_DIR:-$RESULTS_DIR/rpms-debug}"
RESULTING_SRPMS_DIR="${RESULTING_SRPMS_DIR:-$RESULTS_DIR/rpms-src}"
RESULTING_ISO_DIR="${RESULTING_ISO_DIR:-$RESULTS_DIR/iso}"
REPO_DIR="${REPO_DIR:-/mnt/repo}"
REPOURI="${REPOURI:-$REPO_DIR}"
BUILD_ISO_ROOT_DIR="${BUILD_ISO_ROOT_DIR:-$PWD/../../..}"
NPROC="${NPROC:-$(nproc)}"
# сборки могут падать из-за нехватки ресурсов (tmpfs, ulimit),
# поэтому есть смысл их перезапускать по несколько раз
RETRY="${RETRY:-5}"
ROSAVER="${ROSAVER:-2021.15}"
# x86_64, i686, aarch64
ARCH="${ARCH:-x86_64}"
# 0 - не использовать tmpfs, 1 - использовать
# Рекомендуется сделать RAM+swap >= 230 ГБ для NPROC=10 и включить tmpfs
TMPFS="${TMPFS:-0}"

num_failed=
num_ok=

# echo to stderr
echo_err(){
	echo "$@" 1>&2
}

_fatal(){
	echo_err "!FATAL!" "$@"
	return 1
}

# mock to build packages can be run from non-root user from group 'mock'
# but livecd-creator to build the ISO must be run from root,
# so let's better require to run this whole script from root
_check_user(){
	# XXX root may be not 0, but let's assume that it is 0
	[ "$(id -u)" = 0 ] || _fatal "Запустите этот скрипт от root"
}

_check_config(){
	if ! [ "$RETRY" -gt 0 ]; then
		_fatal "RETRY не может быть меньше 1"
	fi
}

_prepare_users(){
	if ! getent passwd "$MOCK_USER" >/dev/null 2>&1; then
		useradd "$MOCK_USER"
	fi
	usermod -a -G mock "$MOCK_USER"
	mkdir -p "$RESULTS_DIR"
	setfacl -m u:"$MOCK_USER":rwx "$RESULTS_DIR"
	# От старых запуском скрипта могли остаться lock-файлы и пр.,
	# нормальных trap-ов пока не написано, поэтому зачищаем
	mkdir -p /var/lib/mock
	rm -fr /var/lib/mock/*
	chown "$MOCK_USER":"$MOCK_USER" /var/lib/mock
	# /var/lib/offlinerebuilder
	local mock_root
	mock_root="$(dirname "$RESULTS_DIR")"
	[ -d "$mock_root" ]
	chown "$MOCK_USER":"$MOCK_USER" "$mock_root"
	chmod 755 "$mock_root"
}

_test_repo_availability(){
	[ -d "$REPO_DIR" ] || _fatal "Не найден локальный репозиторий разработчика ${REPO_DIR}. Проверьте смонтированность диска разработчика."
	find "$REPO_DIR" -type d -name repodata | grep -q repodata || _fatal "Не найдены метаданные локального репозиторяи разработчика ${REPO_DIR}, возможно, диск разработчика смонтирован неправильно."
}

_validate_input_srpms_directory(){
	[ -d "$SRPMS_DIR" ] || _fatal "Каталог с SRPM $SRPMS_DIR не существует!"
	if ! [ "$(find "$SRPMS_DIR" -maxdepth 1 -type f -name '*.src.rpm' | grep -c .)" -ge 1 ]; then
		_fatal "В каталоге с SRPM $SRPMS_DIR отсутствуют файлы *.src.rpm"
	fi
}

_test_writability(){
	# XXX seems that such trap traps after returns from _other_ functions...
	#trap 'rm -fr "$_tmp_d" "$_tmp_f" "$_tmp_r"' RETURN
	sudo -u "$MOCK_USER" mkdir -p "$LOGS_DIR" || _fatal "Ошибка создания каталога для хранения логов $LOGS_DIR"
	local _tmp_d
	if ! _tmp_d="$(sudo -u "$MOCK_USER" mktemp --tmpdir="$LOGS_DIR" --directory)" ; then
		_fatal "Ошибка проверочного создания каталога для логов $_tmp_d"
	fi
	local _tmp_f
	if ! _tmp_f="$(sudo -u "$MOCK_USER" mktemp --tmpdir="$LOGS_DIR")" ; then
		_fatal "Ошибка проверочного создания лог-файла $_tmp_f"
	fi
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_RPMS_TMP" || _fatal "Ошибка создания каталога для хранения результов сборки $RESULTING_RPMS_TMP"
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_RPMS_DIR" || _fatal "Ошибка создания каталога для хранения результов сборки $RESULTING_RPMS_DIR"
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_DEBUG_DIR" || _fatal "Ошибка создания каталога для хранения результов сборки $RESULTING_DEBUG_DIR"
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_SRPMS_DIR" || _fatal "Ошибка создания каталога для хранения результов сборки $RESULTING_SRPMS_DIR"
	local _tmp_r1
	local _tmp_r2
	local _tmp_r3
	local _tmp_r4
	if ! _tmp_r1="$(sudo -u "$MOCK_USER" mktemp --tmpdir="$RESULTING_RPMS_TMP")" ; then
		_fatal "Ошибка проверочного создания файла _tmp_r1"
	fi
	if ! _tmp_r2="$(sudo -u "$MOCK_USER" mktemp --tmpdir="$RESULTING_RPMS_DIR")" ; then
		_fatal "Ошибка проверочного создания файла _tmp_r2"
	fi
	if ! _tmp_r3="$(sudo -u "$MOCK_USER" mktemp --tmpdir="$RESULTING_DEBUG_DIR")" ; then
		_fatal "Ошибка проверочного создания файла _tmp_r3"
	fi
	if ! _tmp_r4="$(sudo -u "$MOCK_USER" mktemp --tmpdir="$RESULTING_SRPMS_DIR")" ; then
		_fatal "Ошибка проверочного создания файла _tmp_r4"
	fi
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_ISO_DIR" || _fatal "Ошибка создания каталога для хранения собранных образов $RESULTING_ISO_DIR"
	# XXX instead of trap
	rm -fr "$_tmp_d" "$_tmp_f" "$_tmp_r1" "$_tmp_r2" "$_tmp_r3" "$_tmp_r4"
}

_validate_logs_directory(){
	if [ "$(find "$LOGS_DIR" -maxdepth 1 -type f -name '*.log' | wc -l)" != 0 ]; then
		_fatal "В каталоге $LOGS_DIR уже есть старые логи сборки, удалите их!"
	fi
}

_choose_mock_dir(){
	local num
	num=$((NPROC+5))
	for i in $(seq 1 "$num")
	do
		local dir=mock_"$i"
		if [ ! -f /var/lib/mock/"$dir".locked ]; then
			touch /var/lib/mock/"$dir".locked
			echo "$dir"
			break
		fi
	done
}

# $1: rpm file
# $2: target arch
# Returns 0 if current arch is not excluded
# Returns 1 is current arch is excluded
_check_srpm_arch(){
	local exclusivearch
	# https://rpm-software-management.github.io/rpm/manual/queryformat.html
	excludearch="$(rpm -qp --qf '[%{excludearch} ]' "$1")"
	[[ "$excludearch" =~ .*"$2".* ]] && return 1
	local exclusivearch
	exclusivearch="$(rpm -qp --qf '[%{exclusivearch} ]' "$1")"
	if [ -z "$exclusivearch" ] || [ "$exclusivearch" = "(none)" ]; then
		return 0
	fi
	! [[ "$exclusivearch" =~ .*"$2".* ]] && return 1
	return 0
}

_run_mock(){
	if ! _check_srpm_arch "$1" "$ARCH"; then
		echo "Пропускаем $1 на архитектуре $ARCH"
		return 0
	fi
	local try=1
	local mock_tmp_log
	mock_tmp_log="$(mktemp --suffix=.log)" || _fatal "Ошибка создания временного лог-файла mock"
	local srpm_dir
	# There may be multiple SRPMs with the same name (e.g. python38-* stuff) producing different binary RPMs
	srpm_dir="$(rpm -qp --qf '%{name}-%{version}-%{release}' "$1")"
	[ -n "$srpm_dir" ] || _fatal "Ошибка получения имени исходного пакета $1"
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_RPMS_TMP/$srpm_dir"
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_RPMS_DIR/$ARCH"
	sudo -u "$MOCK_USER" mkdir -p "$RESULTING_RPMS_DIR/$ARCH"
	# Выбираем одну из директорий, чтобы не создавать взаимных блокировок
	# т.к. выбор происходит одновременно, возникает гонка. Поэтому делаем выбор последовательным с помощью flock.
	local mock_root_dir
	mock_root_dir="$(flock "$LOGS_DIR"/.mock.flock bash -c _choose_mock_dir)"
	[ -n "$mock_root_dir" ] || _fatal "Не удалось выбрать незаблокированный рабочий каталог mock"
	[ -f "$MOCK_CONFIG" ] || _fatal "Не найден конфиг mock $MOCK_CONFIG"
	local MOCK_CONFIG_DIRECTORY
	MOCK_CONFIG_DIRECTORY="$(sudo -u "$MOCK_USER" mktemp --directory)"
	touch "$MOCK_CONFIG_DIRECTORY"/default.cfg
	local mock_tmpfs=False
	if [ "$TMPFS" = 1 ]; then mock_tmpfs=True; fi
	sed "$MOCK_CONFIG" \
		-e "s,@REPOURI@,${REPOURI},g" \
		-e "s,@BASEDIR@,${mock_root_dir},g" \
		-e "s,@CACHEDIR@,/var/lib/mock/${mock_root_dir}.cache,g" \
		-e "s,@ROSAVER@,${ROSAVER},g" \
		-e "s,@ARCH@,${ARCH},g" \
		-e "s,@TMPFS@,${mock_tmpfs},g" \
	> "$MOCK_CONFIG_DIRECTORY"/default.cfg
	chown -R "$MOCK_USER":"$MOCK_USER" "$RESULTING_RPMS_TMP/$srpm_dir"
	while true
	do
		echo "Запуск сборки $srpm_dir ($1) для $ARCH (попытка ${try} из ${RETRY})"
		# shellcheck disable=SC2216
		# shellcheck disable=SC2024
		# piping to true to avoid any console output when running multiple mocks via xargs
		# Некоторые пакеты, например, tar, не хотят собираться от root без специалньых пинков под зад,
		# поэтому запускаем mock от не root, а от пользователя из группы mock
		sudo -u "$MOCK_USER" mock \
			-v \
			--enablerepo main-"${ARCH}" \
			--rebuild \
			--configdir "$MOCK_CONFIG_DIRECTORY" \
			--resultdir "$RESULTING_RPMS_TMP/$srpm_dir" \
			"$1" \
		> "$mock_tmp_log" 2>&1 | true
		local rc=${PIPESTATUS[0]}
		if [ "$rc" = 0 ] || [ "$try" -ge "$RETRY" ]; then break; fi
		if [ "$rc" != 0 ]; then
			echo "Ошибка сборки $1 для $ARCH на попытке ${try} из ${RETRY}, пробуем еще раз"
		fi
		try=$((try+1))
	done
	local log_file_prefix=""
	case ${rc} in
		0 )
			find "$RESULTING_RPMS_TMP/$srpm_dir" -name '*.rpm' | grep -vE -- '-debug(info|source)-|\.src\.rpm$' | xargs -I'{}' mv '{}' "$RESULTING_RPMS_DIR/$ARCH"
			find "$RESULTING_RPMS_TMP/$srpm_dir" -name '*.rpm' | grep -E -- '-debug(info|source)-' | xargs -I'{}' mv '{}' "$RESULTING_DEBUG_DIR/$ARCH"
			find "$RESULTING_RPMS_TMP/$srpm_dir" -name '*.src.rpm' | xargs -I'{}' mv '{}' "$RESULTING_SRPMS_DIR"
			# move logs from mock
			# build.log hw_info.log installed_pkgs.log root.log state.log
			mv "$RESULTING_RPMS_TMP/$srpm_dir" "${LOGS_DIR}/${srpm_dir}.${ARCH}"
			chown -R "$MOCK_USER":"$MOCK_USER" "${LOGS_DIR}/${srpm_dir}.${ARCH}"
			chmod 755 "${LOGS_DIR}/${srpm_dir}.${ARCH}"
			log_file_prefix="ok"
			echo "УСПЕШНО собран $1 для $ARCH"
		;;
		* )
			log_file_prefix="failed"
			echo "ОШИБКА сборки $1 для $ARCH"
		;;
	esac
	rm -f /var/lib/mock/"$mock_root_dir".locked
	local target_log
	target_log="$LOGS_DIR"/"$log_file_prefix"."$(basename "$1")"."$ARCH".log
	mv "$mock_tmp_log" "$target_log"
	# после mv логи принадлежат root и недоступны другим даже для чтения
	chown "$MOCK_USER":"$MOCK_USER" "$target_log"
	chmod 644 "$target_log"
}

_run_mass_build(){
	# TODO: контроль ошибок вне mock внутри функции _run_mock
	find "$SRPMS_DIR" -maxdepth 1 -type f -name '*.src.rpm' | \
	sort -u | \
	xargs -I'{}' -P"$NPROC" bash -c "set -eu; set -f; _run_mock {}"
}

_run_mass_build_subarch(){
	find "$SRPMS_DIR" -maxdepth 1 -type f -name '*.src.rpm' |
	while read -r line
	do
		if grep -q "^$(rpm -qp --qf '%{name}' "$line")$" i686.list; then
			echo "$line"
		fi
	done |
	xargs -I'{}' -P"$NPROC" bash -c "set -eu; set -f; _run_mock {}"
}

_echo_results(){
	echo ""
	echo "РЕЗУЛЬТАТЫ СБОРКИ:"
	num_failed="$(find "$LOGS_DIR" -name 'failed.*.log' | wc -l)"
	num_ok="$(find "$LOGS_DIR" -name 'ok.*.log' | wc -l)"
	echo "Успешно собрано $num_ok пакетов"
	echo "Не удалось собрать $num_failed пакетов"
	if [ "$num_failed" -gt 0 ]; then
		echo "!!! Были ошибки сборки !!!"
	fi
	echo "Логи находятся в каталоге $LOGS_DIR"
}

_build_iso(){
	mkdir -p "$LOGS_DIR"/iso-build-logs/"$1"
	pushd "$BUILD_ISO_ROOT_DIR"
	if [ "$1" = server ]
		then FILTER_PACKAGES="sconfigs-kscreenlocker"
		else FILTER_PACKAGES=""
	fi
	# LC1 means laboratory certification №1
	ABF=0 \
	BUILD_ID=LC1 \
	TMPFS="$TMPFS" \
	DISTROSYNC=0 \
	CUSTOMIZATION_FEATURES="s4" \
	PLATFORM=rosa"$ROSAVER" \
	RESULTS_DIR="$RESULTING_ISO_DIR" \
	ARCH="$(uname -m)" \
	DISABLE_STANDARD_REPOS=1 \
	ADD_REPOS="$RESULTING_RPMS_DIR" \
	DE="$1" \
	FILTER_PACKAGES="$FILTER_PACKAGES" \
	LIVECD_CREATOR_EXTRA_ARGS="--isotype=gpt2" \
	bash ./build-iso-abf.sh 2>&1 | \
	tee "$LOGS_DIR"/iso-build-logs/"$1"/iso.log
	if [ "${PIPESTATUS[0]}" != 0 ]; then
		_fatal "Ошибка сборки образа $1"
		return 1
	fi
	mv -v "$RESULTING_ISO_DIR"/*.{log,xz} "$LOGS_DIR"/iso-build-logs/"$1"
	popd
}

_mk_repo_isos(){
	genisoimage \
		-iso-level 4 \
		-joliet \
		-joliet-long \
		-rational-rock \
		-o "$RESULTING_ISO_DIR"/repo-rosa"${ROSAVER}"-"$ARCH"_bin-rpms_certified.iso \
		"$RESULTING_RPMS_DIR"
	genisoimage \
		-iso-level 4 \
		-joliet \
		-joliet-long \
		-rational-rock \
		-o "$RESULTING_ISO_DIR"/repo-rosa"${ROSAVER}"-"$ARCH"_debug-rpms_certified.iso \
		"$RESULTING_DEBUG_DIR"
	genisoimage \
		-iso-level 4 \
		-joliet \
		-joliet-long \
		-rational-rock \
		-o "$RESULTING_ISO_DIR"/repo-rosa"${ROSAVER}"-"$ARCH"_src-rpms_certified.iso \
		"$RESULTING_SRPMS_DIR"
}

_main(){
	#trap 'rm -fr "$MOCK_CONFIG_DIRECTORY"' RETURN
	_check_user
	_check_config
	_prepare_users
	_test_repo_availability
	_validate_input_srpms_directory
	_test_writability
	_validate_logs_directory
	case "$(uname -m)" in
		x86_64 )
			export ARCH=x86_64
			_run_mass_build
			export ARCH=i686
			_run_mass_build_subarch
		;;
		aarch64 )
			export ARCH=aarch64
			_run_mass_build
		;;
		* )
			_fatal "Неизвестная аппаратная архитектура"
		;;
	esac
	# TODO: rm other temp stuff
	rm -f "$LOGS_DIR"/.mock.flock
	_echo_results
	if [ "$num_failed" != 0 ]; then
		return "$num_failed"
	fi
	createrepo_c "$RESULTING_RPMS_DIR" || _fatal "Ошибка создания метаданных репозитория релизных RPM"
	createrepo_c "$RESULTING_DEBUG_DIR" || _fatal "Ошибка создания метаданных репозитория отладочных RPM"
	createrepo_c "$RESULTING_SRPMS_DIR" || _fatal "Ошибка создания метаданных репозитория исходных RPM"
	_mk_repo_isos || _fatal "Ошибка создания ISO-образов репозиториев"
	echo "ЗАПУСК сборки ISO-образа десктопной ОС"
	if _build_iso plasma5
	then
		echo "УСПЕШНО собран ISO-образ десктопной ОС"
	else
		_fatal "ОШИБКА сборки ISO-образа десктопной ОС!"
	fi
	echo "ЗАПУСК сборки ISO-образа серверной ОС"
	if _build_iso server
	then
		echo "УСПЕШНО собран ISO-образ серверной ОС"
	else
		_fatal "ОШИБКА сборки ISO-образа серверной ОС!"
	fi
	chown -R "$MOCK_USER:$MOCK_USER" "$RESULTS_DIR"
}

_main "$@"
